Skip to content

Share a single MongoClient and fix LiteDB TableCollection - #757

Merged
KrzysztofPajak merged 2 commits into
developfrom
fix/data-layer-defects
Aug 8, 2026
Merged

Share a single MongoClient and fix LiteDB TableCollection#757
KrzysztofPajak merged 2 commits into
developfrom
fix/data-layer-defects

Conversation

@KrzysztofPajak

@KrzysztofPajak KrzysztofPajak commented Aug 8, 2026

Copy link
Copy Markdown
Member

Type: bugfix

Issue

No issues were open for these; both came out of an audit of the data layer. Two independent changes,
one commit each. They are not of equal weight, so to be straight about it:

1. LiteDBRepository.TableCollection<C>() queried a collection named "T" — a real bug.
nameof(T) on a generic type parameter evaluates to the literal string "T", not the entity name:

return Database.GetCollection<C>(nameof(T)).Query().ToEnumerable().AsQueryable();

MongoRepository.TableCollection<C>() already used typeof(T).Name, so the two providers disagreed.

Reach is narrow and worth stating plainly: TableCollection<C>() has exactly one production
consumer, GetGenericQueryHandler in Grand.Module.Api. Triggering the bug needs the LiteDB
provider and the API module enabled, and both are opt-in. In that combination every generic API
query returned an empty result set.

2. A MongoClient was constructed per DI scope — hygiene, not a defect.
src/Core/Grand.Infrastructure/Startup/StartupApplication.cs registered the database as
AddScoped(_ => new MongoClient(clientSettings).GetDatabase(databaseName)), so a client was built
per scope, that is per request.

An earlier draft of this description called that the most impactful defect in the data layer. That
was wrong, and the corrected version is less dramatic: the driver deduplicates the underlying
cluster through ClusterRegistry, so this was not a connection pool per request, and
MongoClientSettings.FromConnectionString already ran once at startup. What actually happened on
every request was a settings clone and freeze, a ClusterKey computation, a lock-protected registry
lookup, and a client wrapper allocation.

That is small. It is still contrary to the driver's documented usage — MongoClient is meant to be a
single, thread-safe instance owning the connection pool and cluster monitoring — and removing it
costs four lines and changes no behaviour.

Solution

  • Use typeof(T).Name in LiteDBRepository.TableCollection<C>(), matching the Mongo
    implementation, with a regression test in Grand.Data.Tests.
  • Register IMongoClient as a singleton and resolve the scoped IMongoDatabase from it. Consumers
    are untouched — they still inject IMongoDatabase, still scoped.

On lifetime, since it is the one thing that could bite: previously the scoped clients were created
by a factory, so the container disposed one per scope. If MongoClient.Dispose tore down the shared
registry cluster, the application would fail continuously — it does not. The singleton is disposed
at shutdown, which is correct. Nothing else in the solution resolves IMongoClient.

Note for reviewers: the parameterless constructors of MongoRepository and MongoStoreFilesContext
still build their own MongoClient. DI only selects those before installation, when
IMongoDatabase is not registered yet — afterwards it picks the IMongoDatabase overloads. They
are deliberately left alone rather than widened into this change.

Breaking changes

None. IMongoClient was not registered before, so the new registration cannot collide, and no
public signature changed. The scoped IMongoDatabase registration is preserved, so every existing
consumer resolves exactly as it did.

Testing

  1. dotnet build ./GrandNode.sln — succeeds.
  2. dotnet test ./src/Tests/Grand.Data.Tests/Grand.Data.Tests.csproj — 46/46 pass.
  3. dotnet test ./src/Tests/Grand.Infrastructure.Tests/Grand.Infrastructure.Tests.csproj — 84/84 pass.
  4. To confirm the new test guards the defect, temporarily put nameof(T) back in
    LiteDBRepository.TableCollection<C>() and re-run step 2.
    TableCollection_ReadsTheEntityCollection fails with expected: 2, actual: 0. Revert.
  5. Mongo runtime check: start the storefront against MongoDB and browse the catalogue, place an
    order, and open the admin panel. Everything resolving IMongoDatabase must behave as before —
    the point of the change is that only the client's lifetime moved.
  6. LiteDB check: set Database:UseLiteDb to true, enable the Grand.Module.Api feature, and call
    a generic API query endpoint. It returns rows; before this change it returned an empty set.

Steps 5 and 6 have not been run here — they need a running instance and, for 6, a LiteDB
installation with the API module switched on.

🤖 Generated with Claude Code

KrzysztofPajak and others added 2 commits August 8, 2026 17:21
The database was registered as
AddScoped(_ => new MongoClient(clientSettings).GetDatabase(databaseName)),
so a MongoClient was constructed for every DI scope, that is every request.

The driver deduplicates the underlying cluster through ClusterRegistry, so this
was not a connection pool per request, but the client is documented as a
single, thread-safe instance that owns the pool and cluster monitoring. Building
one per request allocates and freezes settings on a hot path for no benefit.

IMongoClient is now a singleton and the scoped IMongoDatabase is taken from it,
so consumers are unchanged.

The parameterless constructors in MongoRepository and MongoStoreFilesContext
still build their own client; DI only selects them before installation, when
IMongoDatabase is not registered yet, so they are left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nameof(T) on a generic type parameter is the literal string "T", so
TableCollection<C>() queried a collection named "T" rather than the one named
after the entity, and always came back empty.

The Mongo implementation already uses typeof(T).Name. The method backs the
generic API query handler, so under LiteDB every $-query returned nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 8, 2026 15:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@KrzysztofPajak KrzysztofPajak changed the title Fix data layer defects: per-scope MongoClient and LiteDB TableCollection Share a single MongoClient and fix LiteDB TableCollection Aug 8, 2026
@KrzysztofPajak
KrzysztofPajak merged commit 3199d91 into develop Aug 8, 2026
6 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the fix/data-layer-defects branch August 8, 2026 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants